import storageManager.*;
import table.*;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TinySQL {
    public static MainMemory mem = new MainMemory();
    public static Disk disk = new Disk();
    public static SchemaManager schema_manager = new SchemaManager(mem, disk);
    public static HashMap<String, Integer> numberOfBlocksForRelation = new HashMap<String, Integer>();
    private static LRUCache<String, Block> lru_cache = new LRUCache<String, Block>(10);
    private static HashMap<Integer, String> memoryToRelationMap = new HashMap<Integer, String>();
    private static HashMap<String, Integer> relationToMemoryMap = new HashMap<String, Integer>();


    public static String getTableName_BlockIndex(Block mem_block) {
        for (Map.Entry<String, Block> b : lru_cache.getAll())
            if (b.getValue().equals(mem_block))
                return b.getKey();
        return null;
    }

    public static void main(String args[]) {
        long start = System.currentTimeMillis();
        String query;
        BufferedReader br = null;
        try {
            System.out.print("Reading input from - ");
            if (isConsoleMode(args)) {
                br = new BufferedReader(new InputStreamReader(System.in));
                System.out.println("\"stdin\"");
            } else {
                br = new BufferedReader(new InputStreamReader(new FileInputStream(args[0])));
                System.out.println("\"" + args[0] + "\"");

            }
        } catch (FileNotFoundException e) {
            System.err.println("FileError : " + e.getMessage());
            System.exit(-1);
        }
        int i = 1;

        TinySQLParser parser = null;


        while (true) {
            try {
                System.out.print(i++ + ">> ");

                query = br.readLine();
                if (query == null)
                    break;

                if (query.startsWith("//")) {
                    continue;
                }

                if (parser == null) parser = new TinySQLParser(query);
                else
                    parser.ReInit(new StringReader(query));
                if (!isConsoleMode(args))
                    System.out.println(query);
                parser.Statement();
                TinySQLParser.jjtree.rootNode().interpret();

            } catch (IOException e) {
                System.err.println("IOError : " + e.getMessage());
                System.exit(-1);
            } catch (TokenMgrError t) {
                System.err.println("LexicalError : " + t.getMessage());
            }
            catch (ParseException e) {
                System.err.println("ParseError : " + e.getMessage());
            } catch (TinySQLException e) {
                System.err.println(e.getMessage());
            }
        }

        long elapsedTimeMillis = System.currentTimeMillis() - start;
        System.out.print("Computer elapse time = " + elapsedTimeMillis + " ms" + "\n");
        System.out.print("Calculated elapse time = " + disk.getDiskTimer() + " ms" + "\n");
        System.out.print("Calculated Disk I/Os = " + disk.getDiskIOs() + "\n");
    }

    public static boolean isConsoleMode(String[] args) {
        return args.length == 0;
    }

    public static Relation createTable(String tableName, ArrayList<String> arguments) throws TableAlreadyExistsException {

        if (schema_manager.relationExists(tableName))
            throw new TableAlreadyExistsException(tableName);

        ArrayList<String> fieldNames = new ArrayList<String>();
        ArrayList<FieldType> fieldTypes = new ArrayList<FieldType>();

        int i = 0;
        for (String arg : arguments) {
            if (i % 2 == 0)
                fieldNames.add(arg);
            else
                fieldTypes.add(FieldType.valueOf(arg));
            i++;
        }

        if (i % 2 != 0)
            throw new IllegalArgumentException("createTable called with invalid number of arguments");


        Relation result = schema_manager.createRelation(tableName, new Schema(fieldNames, fieldTypes));

        if (result != null) {
            int memoryIndex = createNewEmptyBlock(tableName, 0);
            incrementNumberOfBlocksForRelation(tableName);
            updateMemoryIndex(memoryIndex, tableName + 0);
        }

        return result;
    }

    public static boolean dropTable(String tableName) throws TableDoesNotExistException {
        if (!schema_manager.relationExists(tableName))
            throw new TableDoesNotExistException(tableName);
        deleteAllBlocksInMemoryOfRelation(tableName);
        return schema_manager.deleteRelation(tableName);
    }

    private static void deleteAllBlocksInMemoryOfRelation(String tableName) {

        for (Map.Entry<String, Block> entry : lru_cache.getAll()) {
            String[] split_array = entry.getKey().split("\\.");
            if (split_array[0].equals(tableName)) {
                int blockIndex;
                blockIndex = Integer.parseInt(split_array[1]);
                if (blockIndex < 0)
                    throw new IllegalArgumentException(entry.getKey() + "is an invalid string format for lru_cache key");
                entry.getValue().clear();
                updateMemoryIndex(null, tableName + "." + blockIndex);
                lru_cache.remove(entry.getKey());
            }
        }

        numberOfBlocksForRelation.remove(tableName);
    }


    public static void insertIntoTable(String tableName, String[] argList, ArrayList<MyTuple> myTuples) throws TableDoesNotExistException, InvalidSchemaException, InvalidBlockOperationException {
        Relation relation = schema_manager.getRelation(tableName);
        if (!schema_manager.relationExists(tableName))
            throw new TableDoesNotExistException(tableName);

        int lastBlockIndex = numberOfBlocksForRelation.get(tableName) - 1;
        Integer memoryIndex = getMemoryIndex(tableName + lastBlockIndex);
        if (memoryIndex == null) {//block not in memory
            //getBlockIntoMemory
            memoryIndex = createNewEmptyBlock(tableName, lastBlockIndex);
            relation.getBlock(lastBlockIndex, memoryIndex);
            lru_cache.put(tableName + "." + lastBlockIndex, mem.getBlock(memoryIndex));
            updateMemoryIndex(memoryIndex, tableName + lastBlockIndex);
        }

        for (MyTuple myTuple : myTuples) {
            Block lastBlock = getBlockFromMemory(tableName, lastBlockIndex);

            if (lastBlock.isFull()) {
                lastBlockIndex++;
                memoryIndex = createNewEmptyBlock(tableName, lastBlockIndex);
                incrementNumberOfBlocksForRelation(tableName);
                updateMemoryIndex(memoryIndex, tableName + (lastBlockIndex));
                lastBlock = getBlockFromMemory(tableName, lastBlockIndex);
            }

            int i = 0;
            Tuple tuple = relation.createTuple();
            for (String arg : argList) {

                Field field = myTuple.values.get(i++);
                boolean result = true;
                if (isNull(field)) {
                } else if (field.type == FieldType.STR20)
                    result = tuple.setField(arg, field.str);
                else if (field.type == FieldType.INT)
                    result = tuple.setField(arg, field.integer);
                if (!result)
                    throw new InvalidSchemaException("SchemaError: Unable to set "  + field.type + " value for " + arg + " here.");
            }
            if (!lastBlock.appendTuple(tuple)) {
                throw new InvalidBlockOperationException("unable to append tuple " + tuple + " to block " + lastBlockIndex);
            }

        }

    }

    public static boolean isNull(Field field) {
        return (field.type == FieldType.INT && field.integer == Integer.MIN_VALUE); //TODO Hack! Must Fix.
    }

    private static Integer createNewEmptyBlock(String tableName, int diskBlockIndex) {

        int blockIndex;
        boolean isCacheFull = false;
        if (lru_cache.usedEntries() == lru_cache.cacheSize)
            isCacheFull = true;

        lru_cache.put(tableName + "." + diskBlockIndex, null);

        if (!isCacheFull)
            blockIndex = lru_cache.usedEntries() - 1;
        else {
            if (lru_cache.replacedIndex == null) {
                throw new NullPointerException("ReplacedIndex null");
            }
            blockIndex = lru_cache.replacedIndex;
        }

        Block block = mem.getBlock(blockIndex);
        block.clear();
        lru_cache.put(tableName + "." + diskBlockIndex, block);
        return blockIndex;
    }

    private static Block getBlockFromMemory(String relation_name, int block_index) {
        return lru_cache.get(relation_name + "." + block_index);
    }

    private static void updateMemoryIndex(Integer index, String key) {
        if (key != null)
            relationToMemoryMap.put(key, index);
        if (index != null)
            memoryToRelationMap.put(index, key);
    }

    private static Integer getMemoryIndex(String key) {
        return relationToMemoryMap.get(key);
    }

    private static String getTableName_BlockIndex(Integer index) {
        return memoryToRelationMap.get(index);
    }


    public static void incrementNumberOfBlocksForRelation(String relationName) {
        Integer value = numberOfBlocksForRelation.get(relationName);
        if (value == null) {
            value = 0;
        }
        numberOfBlocksForRelation.put(relationName, ++value);
    }

    public static void decrementNumberOfBlocksForRelation(String relationName) {
        Integer value = numberOfBlocksForRelation.get(relationName);
        if (value == null || value == 0) {
            throw new IllegalArgumentException(relationName + " cannot have less than zero blocks");
        }
        numberOfBlocksForRelation.put(relationName, --value);

    }


    public static boolean dump(Block replaced) {
        String key = getTableName_BlockIndex(replaced);
        String[] split_array = key.split("\\.");
        String tableName = split_array[0];
        int blockIndex;
        try {
            blockIndex = Integer.parseInt(split_array[1]);
            if (blockIndex < 0)
                throw new IllegalArgumentException(key + "is an invalid string format for lru_cache key");
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        Relation updatedRelation = schema_manager.getRelation(tableName);
        updatedRelation.setBlock(blockIndex, getMemoryIndex(key.replace(".", "")));

        lru_cache.replacedIndex = getMemoryIndex(key.replace(".", ""));

        updateMemoryIndex(null, key.replace(".", ""));
        return true;

    }

    public static void deleteFromTable(String tableName, Node node) throws TableDoesNotExistException {
        Relation relation = schema_manager.getRelation(tableName);
        if (!schema_manager.relationExists(tableName))
            throw new TableDoesNotExistException(tableName);

        int numberOfBlocks = numberOfBlocksForRelation.get(tableName);
        boolean checked[] = new boolean[numberOfBlocks];

        for (Map.Entry<String, Block> entry : lru_cache.getAll()) {
            String[] split_array = entry.getKey().split("\\.");
            if (split_array[0].equals(tableName)) {
                int blockIndex;
                blockIndex = Integer.parseInt(split_array[1]);
                if (blockIndex < 0)
                    throw new IllegalArgumentException(entry.getKey() + "is an invalid string format for lru_cache key");
                if (processBlockForDeletion(relation, entry.getValue(), node)) {
                    compactBlocks(tableName);
                }
                checked[blockIndex] = true;
            }
        }

        for (int i = 0; i < numberOfBlocks; i++) {
            if (checked[i]) {
            } else {
                int memoryIndex = createNewEmptyBlock(tableName, i);
                relation.getBlock(i, memoryIndex);
                lru_cache.put(tableName + "." + i, mem.getBlock(memoryIndex));
                updateMemoryIndex(memoryIndex, tableName + i);
                if (processBlockForDeletion(relation, getBlockFromMemory(tableName, i), node)) {
                    compactBlocks(tableName);
                }

            }
        }

    }

    private static void compactBlocks(String tableName) throws TableDoesNotExistException {
        Relation relation = schema_manager.getRelation(tableName);
        if (!schema_manager.relationExists(tableName))
            throw new TableDoesNotExistException(tableName);
        if (isCompactionNecessary(tableName)) {
            Block freeBlock, liveBlock;
            int blockSize = relation.getSchema().getTuplesPerBlock();
            int freeIndex = 0, liveIndex = blockSize - 1;
            int freeBlockNo = 0, liveBlockNo = relation.getNumOfBlocks() - 1;
            int i = 0, j = (relation.getNumOfBlocks() * blockSize);
            int memoryIndex;
            //getting freeBlock
            memoryIndex = createNewEmptyBlock(tableName, freeBlockNo);
            relation.getBlock(freeBlockNo, memoryIndex);
            lru_cache.put(tableName + "." + freeBlockNo, mem.getBlock(memoryIndex));
            updateMemoryIndex(memoryIndex, tableName + freeBlockNo);
            freeBlock = getBlockFromMemory(tableName, freeBlockNo);
            //getting liveBlock
            memoryIndex = createNewEmptyBlock(tableName, liveBlockNo);
            relation.getBlock(liveBlockNo, memoryIndex);
            lru_cache.put(tableName + "." + liveBlockNo, mem.getBlock(memoryIndex));
            updateMemoryIndex(memoryIndex, tableName + liveBlockNo);
            liveBlock = getBlockFromMemory(tableName, liveBlockNo);
            while (i != j) {
                Tuple free, live;
                free = freeBlock.getTuple(freeIndex);
                live = liveBlock.getTuple(liveIndex);
                if ((free.isNull()) && (!live.isNull())) {
                    freeBlock.setTuple(freeIndex, live);
                    live.invalidate();
                    i++;
                    j--;
                    freeIndex++;
                    liveIndex--;
                } else if (!free.isNull()) {
                    i++;
                    freeIndex++;
                } else {
                    j--;
                    liveIndex--;
                }
                if (freeIndex == blockSize - 1) {
                    freeIndex = 0;
                    freeBlockNo++;
                    memoryIndex = createNewEmptyBlock(tableName, freeBlockNo);
                    relation.getBlock(freeBlockNo, memoryIndex);
                    lru_cache.put(tableName + "." + freeBlockNo, mem.getBlock(memoryIndex));
                    updateMemoryIndex(memoryIndex, tableName + freeBlockNo);
                    freeBlock = getBlockFromMemory(tableName, freeBlockNo);
                }
                if (liveIndex == -1) {
                    //decrement number of blocks for relation ?
                    liveIndex = blockSize - 1;
                    liveBlockNo--;
                    memoryIndex = createNewEmptyBlock(tableName, liveBlockNo);
                    relation.getBlock(liveBlockNo, memoryIndex);
                    lru_cache.put(tableName + "." + liveBlockNo, mem.getBlock(memoryIndex));
                    updateMemoryIndex(memoryIndex, tableName + liveBlockNo);
                    liveBlock = getBlockFromMemory(tableName, liveBlockNo);
                }
            }
        }
    }

    private static boolean isCompactionNecessary(String tableName) {
        return !(numberOfBlocksForRelation.get(tableName) != null || lru_cache.cacheSize == 0);
    }

    private static boolean processBlockForDeletion(Relation relation, Block blockFromMemory, Node node) {
        int i = 0;
        boolean result = false;
        if (isHole(blockFromMemory)) return false;
        for (Tuple t : blockFromMemory.getTuples()) {
            for (String field_name : relation.getSchema().getFieldNames()) {
                Field field = t.getField(field_name);
                if (isNull(field)) {
                } else if (field.type == FieldType.INT) {
                    MyNode.symtab.put(relation.getRelationName() + "." + field_name, field.integer);
                    MyNode.symtab.put(field_name, field.integer);
                } else if (field.type == FieldType.STR20) {
                    MyNode.symtab.put(relation.getRelationName() + "." + field_name, field.str);
                    MyNode.symtab.put(field_name, field.str);
                }
            }
            if (checkValue(node)) {
                result = true;
                blockFromMemory.invalidateTuple(i);
            }
            i++;
        }
        return result;
    }

    private static boolean checkValue(Node node) {
        if (node == null) {
            MyNode.symtab.clear();
            return true;
        }
        try {
            node.interpret();
        } catch (TinySQLException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
        Boolean result = (Boolean) MyNode.stack[MyNode.top];

        MyNode.top = 0;
        MyNode.stack = new Object[1024];

        return result;
    }

    public static Table selectFromTable(String[] tableList, List<String> selectList, Node node) throws TableDoesNotExistException {
        Table table = null;

        // manage selectList
        if (isSelectAllFields(selectList)) {
            selectList.clear();
            for (String tableName : tableList) {
                Relation relation = schema_manager.getRelation(tableName);
                for (String str : relation.getSchema().getFieldNames())
                    selectList.add(tableName + "." + str);
            }
        } else {
            ArrayList<String> tempList = new ArrayList<String>();
            tempList.addAll(selectList);
            selectList.clear();

            for (String tableName : tableList) {
                Relation relation = schema_manager.getRelation(tableName);
                for (String str : relation.getSchema().getFieldNames())
                    if (tempList.contains(str) || tempList.contains(tableName + "." + str))
                        selectList.add(tableName + "." + str);
            }

        }


        if (tableList.length == 1) {

            ArrayList<MyTuple> tuples = new ArrayList<MyTuple>();
            String tableName = tableList[0];
            table = new Table();
            selectFromSingleTable(node, table, tuples, tableName);
            table.add(tuples);

        } else {


            for (String tableName : tableList) {
                Table currTable = new Table();
                ArrayList<MyTuple> tuples = new ArrayList<MyTuple>();
                selectFromSingleTable(null, currTable, tuples, tableName);
                currTable.add(tuples);
                if (table == null)
                    table = currTable;
                else
                    table = Table.joinTables(table, currTable);
            }

            ArrayList<MyTuple> resultTuples = new ArrayList<MyTuple>();

            for (MyTuple t : table.tuples) {
                for (int i = 0; i < table.columnNames.size(); i++) {
                    Field field = t.values.get(i);
                    if (isNull(field)) {
                    } else if (field.type == FieldType.INT) {
                        MyNode.symtab.put(table.columnNames.get(i), field.integer);
                    } else if (field.type == FieldType.STR20) {
                        MyNode.symtab.put(table.columnNames.get(i), field.str);
                    }
                }
                if (checkValue(node))
                    resultTuples.add(t);

            }

            table.tuples = resultTuples;
        }

        return table;
    }

    private static void selectFromSingleTable(Node node, Table table, ArrayList<MyTuple> tuples, String tableName) throws TableDoesNotExistException {
        Relation relation = schema_manager.getRelation(tableName);
        table.addColumnNames(tableName, relation.getSchema().getFieldNames());
        if (!schema_manager.relationExists(tableName))
            throw new TableDoesNotExistException(tableName);

        int numberOfBlocks = numberOfBlocksForRelation.get(tableName);
        boolean checked[] = new boolean[numberOfBlocks];

        for (Map.Entry<String, Block> entry : lru_cache.getAll()) {
            String[] split_array = entry.getKey().split("\\.");
            if (split_array[0].equals(tableName)) {
                int blockIndex;
                blockIndex = Integer.parseInt(split_array[1]);
                if (blockIndex < 0)
                    throw new IllegalArgumentException(entry.getKey() + "is an invalid string format for lru_cache key");
                tuples.addAll(processBlockForSelection(relation, entry.getValue(), node));
                checked[blockIndex] = true;
            }
        }

        for (int i = 0; i < numberOfBlocks; i++) {
            if (checked[i]) {
            } else {
                int memoryIndex = createNewEmptyBlock(tableName, i);
                relation.getBlock(i, memoryIndex);
                lru_cache.put(tableName + "." + i, mem.getBlock(memoryIndex));
                updateMemoryIndex(memoryIndex, tableName + i);
                tuples.addAll(processBlockForSelection(relation, getBlockFromMemory(tableName, i), node));
            }
        }
    }

    private static boolean isSelectAllFields(List<String> selectList) {
        return selectList.size() == 1 && selectList.get(0).equals("*");
    }

    private static ArrayList<MyTuple> processBlockForSelection(Relation relation, Block blockFromMemory, Node node) {
        ArrayList<MyTuple> tuples = new ArrayList<MyTuple>();
        if (isHole(blockFromMemory)) return tuples; //TODO must not have holes?! Hack must be changed later
        for (Tuple t : blockFromMemory.getTuples()) {
            for (String field_name : relation.getSchema().getFieldNames()) {
                Field field = t.getField(field_name);
                if (isNull(field)) {
                } else if (field.type == FieldType.INT) {
                    MyNode.symtab.put(relation.getRelationName() + "." + field_name, field.integer);
                    MyNode.symtab.put(field_name, field.integer);
                } else if (field.type == FieldType.STR20) {
                    MyNode.symtab.put(relation.getRelationName() + "." + field_name, field.str);
                    MyNode.symtab.put(field_name, field.str);
                }
            }
            if (checkValue(node))
                tuples.add(new MyTuple(getAllFields(t)));
        }
        return tuples;
    }

    private static boolean isHole(Block blockFromMemory) {
        return blockFromMemory.toString().equalsIgnoreCase("(hole)");
    }

    private static ArrayList<Field> getAllFields(Tuple t) {
        ArrayList<Field> fields = new ArrayList<Field>();
        for (int i = 0; i < t.getNumOfFields(); i++)
            fields.add(new Field(t.getField(i)));
        return fields;
    }

}